/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ihuoui.mq.store;

//import com.sun.jna.NativeLong;
//import com.sun.jna.Pointer;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MappedFile {
	public static final int OS_PAGE_SIZE = 1024 * 4;
	protected static final Logger log = LoggerFactory.getLogger(MappedFile.class);

	private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);

	private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
	private static final byte[] FlagCode = new byte[] { 0x00, (byte) 0xff, 0x00, (byte) 0xff };

	protected AtomicInteger wrotePosition;
	protected AtomicInteger readPosition;
	// ADD BY ChenYang
	protected AtomicInteger committedPosition;
	private AtomicInteger flushedPosition;
	protected int fileSize;
	protected FileChannel fileChannel;
	/**
	 * Message will put to here first, and then reput to FileChannel if
	 * writeBuffer is not null.
	 */
	protected ByteBuffer writeBuffer = null;
	private String fileName;
	private long fileFromOffset;
	private File file;
	private MappedByteBuffer mappedByteBuffer;
	private volatile long storeTimestamp = 0;
	private boolean firstCreateInQueue = false;

	public MappedFile() {
		this.wrotePosition = new AtomicInteger(0);
		this.committedPosition = new AtomicInteger(0);
		this.flushedPosition = new AtomicInteger(0);
		this.readPosition = new AtomicInteger(0);
	}

	public MappedFile(final String fileName, final int fileSize) throws IOException {
		init(fileName, fileSize);
		this.wrotePosition = new AtomicInteger(0);
		this.committedPosition = new AtomicInteger(0);
		this.flushedPosition = new AtomicInteger(0);
		this.readPosition = new AtomicInteger(0);
	}

	public static void ensureDirOK(final String dirName) {
		if (dirName != null) {
			File f = new File(dirName);
			if (!f.exists()) {
				boolean result = f.mkdirs();
				log.info(dirName + " mkdir " + (result ? "OK" : "Failed"));
			}
		}
	}

	public static void clean(final ByteBuffer buffer) {
		if (buffer == null || !buffer.isDirect() || buffer.capacity() == 0)
			return;
		invoke(invoke(viewed(buffer), "cleaner"), "clean");
	}

	private static Object invoke(final Object target, final String methodName, final Class<?>... args) {
		return AccessController.doPrivileged(new PrivilegedAction<Object>() {
			public Object run() {
				try {
					Method method = method(target, methodName, args);
					method.setAccessible(true);
					return method.invoke(target);
				} catch (Exception e) {
					throw new IllegalStateException(e);
				}
			}
		});
	}

	private static Method method(Object target, String methodName, Class<?>[] args) throws NoSuchMethodException {
		try {
			return target.getClass().getMethod(methodName, args);
		} catch (NoSuchMethodException e) {
			return target.getClass().getDeclaredMethod(methodName, args);
		}
	}

	private static ByteBuffer viewed(ByteBuffer buffer) {
		String methodName = "viewedBuffer";

		Method[] methods = buffer.getClass().getMethods();
		for (int i = 0; i < methods.length; i++) {
			if (methods[i].getName().equals("attachment")) {
				methodName = "attachment";
				break;
			}
		}

		ByteBuffer viewedBuffer = (ByteBuffer) invoke(buffer, methodName);
		if (viewedBuffer == null)
			return buffer;
		else
			return viewed(viewedBuffer);
	}

	public static int getTotalMappedFiles() {
		return TOTAL_MAPPED_FILES.get();
	}

	public static long getTotalMappedVirtualMemory() {
		return TOTAL_MAPPED_VIRTUAL_MEMORY.get();
	}

	@SuppressWarnings("resource")
	private void init(final String fileName, final int fileSize) throws IOException {
		this.fileName = fileName;
		this.fileSize = fileSize;
		this.file = new File(fileName);
		boolean ok = false;

		ensureDirOK(this.file.getParent());

		try {
			this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
			this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
			TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
			TOTAL_MAPPED_FILES.incrementAndGet();
			ok = true;
		} catch (FileNotFoundException e) {
			log.error("create file channel " + this.fileName + " Failed. ", e);
			throw e;
		} catch (IOException e) {
			log.error("map file " + this.fileName + " Failed. ", e);
			throw e;
		} finally {
			if (!ok && this.fileChannel != null) {
				this.fileChannel.close();
			}
		}
	}

	public long getLastModifiedTimestamp() {
		return this.file.lastModified();
	}

	public int getFileSize() {
		return fileSize;
	}

	public FileChannel getFileChannel() {
		return fileChannel;
	}

	/**
	
	 */
	public long getFileFromOffset() {
		return this.fileFromOffset;
	}

	/**
	
	 *
	
	 */
	public static int byte2int(byte[] res) {
		// 一个byte数据左移24位变成0x??000000，再右移8位变成0x00??0000

		int targets = (res[0] & 0xff) | ((res[1] << 8) & 0xff00) // | 表示安位或
				| ((res[2] << 24) >>> 8) | (res[3] << 24);
		return targets;
	}

	public static byte[] int2byte(int res) {
		byte[] targets = new byte[4];

		targets[0] = (byte) (res & 0xff);// 最低位
		targets[1] = (byte) ((res >> 8) & 0xff);// 次低位
		targets[2] = (byte) ((res >> 16) & 0xff);// 次高位
		targets[3] = (byte) (res >>> 24);// 最高位,无符号右移。
		return targets;
	}

	public int appendMessage(final byte[] data) {
		int currentPos = this.wrotePosition.get();
		int saveDataLen=data.length + int2byte(data.length).length + FlagCode.length;
		if ((currentPos + saveDataLen) <= this.fileSize) {
			try {
				//do not change +4
				this.fileChannel.position(currentPos + 4);
//				 System.out.println("write len:" + saveDataLen);
				// System.out.println("write datas:" +
				// UtilAll.bytes2string(data));
				this.fileChannel.write(ByteBuffer.wrap(FlagCode));
				this.fileChannel.write(ByteBuffer.wrap(int2byte(data.length)));
				this.fileChannel.write(ByteBuffer.wrap(data));
			} catch (Throwable e) {
				log.error("Error occurred when append message to mappedFile.", e);
			}
			this.wrotePosition.addAndGet(saveDataLen);
			return saveDataLen;
			// return wrotePosition;
		}

		return -1;
	}

	/**
	 * @param flushLeastPages
	 * @return The current flushed position
	 */
	public int flush(final int flushLeastPages) {
		if (this.isAbleToFlush(flushLeastPages)) {
			int value = getReadPosition();
			try {
				// We only append data to fileChannel or mappedByteBuffer, never
				// both.
				if (writeBuffer != null || this.fileChannel.position() != 0) {
					this.fileChannel.force(false);
				} else {
					this.mappedByteBuffer.force();
				}
			} catch (Throwable e) {
				log.error("Error occurred when force data to disk.", e);
			}

			this.flushedPosition.set(value);
		}
		return this.getFlushedPosition();
	}

	public int commit(final int commitLeastPages) {
		if (writeBuffer == null) {
			// no need to commit data to file channel, so just regard
			// wrotePosition as committedPosition.
			return this.wrotePosition.get();
		}
		if (this.isAbleToCommit(commitLeastPages)) {
			commit0(commitLeastPages);
		}
		return this.committedPosition.get();
	}

	protected void commit0(final int commitLeastPages) {
		int writePos = this.wrotePosition.get();
		int lastCommittedPosition = this.committedPosition.get();

		if (writePos - this.committedPosition.get() > 0) {
			try {
				ByteBuffer byteBuffer = writeBuffer.slice();
				byteBuffer.position(lastCommittedPosition);
				byteBuffer.limit(writePos);
				this.fileChannel.position(lastCommittedPosition);
				this.fileChannel.write(byteBuffer);
				this.committedPosition.set(writePos);
			} catch (Throwable e) {
				log.error("Error occurred when commit data to FileChannel.", e);
			}
		}
	}

	private boolean isAbleToFlush(final int flushLeastPages) {
		int flush = this.flushedPosition.get();
		int write = getReadPosition();

		if (this.isFull()) {
			return true;
		}

		if (flushLeastPages > 0) {
			return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
		}

		return write > flush;
	}

	protected boolean isAbleToCommit(final int commitLeastPages) {
		int flush = this.committedPosition.get();
		int write = this.wrotePosition.get();

		if (this.isFull()) {
			return true;
		}

		if (commitLeastPages > 0) {
			return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
		}

		return write > flush;
	}

	public int getFlushedPosition() {
		return flushedPosition.get();
	}

	public void setFlushedPosition(int pos) {
		this.flushedPosition.set(pos);
	}

	public boolean isFull() {
		return this.fileSize == this.wrotePosition.get();
	}

	public int getWrotePosition() {
		return wrotePosition.get();
	}

	public void setWrotePosition(int pos) {
		this.wrotePosition.set(pos);
	}

	/**
	 * @return The max position which have valid data
	 */
	public int getReadPosition() {
		return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
	}

	public void setCommittedPosition(int pos) {
		this.committedPosition.set(pos);
	}

	public void warmMappedFile(int pages) {
		long beginTime = System.currentTimeMillis();
		ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
		int flush = 0;
		long time = System.currentTimeMillis();
		for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
			byteBuffer.put(i, (byte) 0);
			// force flush when flush disk type is sync
			if (true) {
				if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
					flush = i;
					mappedByteBuffer.force();
				}
			}

			// prevent gc
			if (j % 1000 == 0) {
				log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
				time = System.currentTimeMillis();
				try {
					Thread.sleep(0);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}

		// force flush when prepare load finished
		if (true) {
			log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}", this.getFileName(),
					System.currentTimeMillis() - beginTime);
			mappedByteBuffer.force();
		}
		log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
				System.currentTimeMillis() - beginTime);

	}

	/**
	 * @throws IOException
	 * 
	 */
	public MappedFileRead getMessByPosition(int pos) throws IOException {
		int newPos=pos;
//		if (pos==0) {
//			//search flag
//			for (int i = 0; i < FlagCode.length; i++) {
//				byte one = this.mappedByteBuffer.get(newPos + i + 4);
//				if (one != FlagCode[i]) {
//					MappedFileRead read = new MappedFileRead();
//					read.setFlagCode(false);
//					return read;
//				}
//			}
//		}
		
		for (int i = 0; i < FlagCode.length; i++) {
			byte one = this.mappedByteBuffer.get(newPos + i + 4);
			if (one != FlagCode[i]) {
				MappedFileRead read = new MappedFileRead();
				read.setFlagCode(false);
				return read;
			}
		}
		byte[] lens = new byte[4];
		for (int i = 0; i < lens.length; i++) {
			byte len = this.mappedByteBuffer.get(newPos + i + 4 + 4);
			lens[i] = len;
		}
		byte[] datas = new byte[byte2int(lens)];
		for (int i = 0; i < byte2int(lens); i++) {
			byte d = this.mappedByteBuffer.get(newPos + i + 4 + 4 + 4);
			datas[i] = d;
		}
		int curr = this.readPosition.addAndGet(newPos + 4 + byte2int(lens)+4);
//		System.out.println("read  file:" + this.fileName+" len:" +( 4 + byte2int(lens)+4));
//		System.out.println("read  len:" + byte2int(lens));
//		System.out.println("read  datas:" + UtilAll.bytes2string(datas));
		MappedFileRead read = new MappedFileRead();
		read.setReadData(datas);
		read.setReadPosition(curr);
		read.setReadLen(4 + byte2int(lens)+4);
		return read;
	}

	@SuppressWarnings("unused")
	private byte[] getMess(int pos, int len) throws IOException {
		byte[] data = new byte[len];
		for (int i = 0; i < len; i++) {
			byte read = this.mappedByteBuffer.get(pos + i);
			data[i] = read;
		}
//		System.out.println("mess len:" + UtilAll.bytes2string(data));
		return data;
	}

	public String getFileName() {
		return fileName;
	}

	public MappedByteBuffer getMappedByteBuffer() {
		return mappedByteBuffer;
	}

	public ByteBuffer sliceByteBuffer() {
		return this.mappedByteBuffer.slice();
	}

	public long getStoreTimestamp() {
		return storeTimestamp;
	}

	public boolean isFirstCreateInQueue() {
		return firstCreateInQueue;
	}

	public void setFirstCreateInQueue(boolean firstCreateInQueue) {
		this.firstCreateInQueue = firstCreateInQueue;
	}

	@Override
	public String toString() {
		return this.fileName;
	}
}
